使用者在對話中不一定會一次給齊所有條件。例如顧客想退貨卻沒給訂單編號,系統無法直接進入退款流程,必須先停下來詢問單號;又或者顧客只說「推薦背包」而沒有提供預算,系統也需要主動提問來收斂範圍。
在上一篇,我們實作了單輪的意圖分類與防護分流。當顧客缺少單號或資訊不足時,系統能將流程導向補問節點。然而,單輪架構在發問後就抵達 END 結束了——當顧客回覆「ORD-1234」時,系統無法保留剛才的上下文,導致對話直接中斷。
真實的客服系統必須具備「多輪對話」的能力:發出問題後停下來等待顧客回答,收到回答後接續先前的狀態繼續處理。
在這篇文章中,我們會處理:
interrupt() 暫停流程並持久化狀態,待使用者回答後以 Command(resume=...) 喚醒接續執行。多輪對話的流程如下圖所示:
範例程式碼位於 ai-agent-sample/langgraph/langgraph-multi-turn-clarification/。
對話意圖與契約定義在 models.py。之前 AI 在分析使用者訊息後,會回傳代表判定意圖的 IntentDecision。現在為了支援多輪對話,我們進一步擴充這個結構——新增兩個欄位,讓 AI 不只要辨識意圖,還要同步決定「是否需要追問」(needs_clarification),並在條件不足時動態產生「要問什麼」(clarification_question):
class IntentDecision(BaseModel):
model_config = ConfigDict(extra="forbid")
intent: Intent
confidence: float = Field(ge=0, le=1)
order_id: str | None = Field(default=None, pattern=r"^ORD-\d{4}$")
needs_clarification: bool # 本篇新增:AI 判斷是否需要追問
clarification_question: str | None = Field(default=None, max_length=200) # 本篇新增:AI 動態產生的追問內容
evidence: str = Field(min_length=1, max_length=200)
有了這兩個欄位,資料契約就具備了描述「是否需要追問」與「問題內容」的能力。
定義好資料契約後,下一步是在 classifier.py 實作分類器與 System Prompt。
光有欄位,AI 並不知道何時該把 needs_clarification 設為 true。若缺乏明確指引,模型容易陷入每句都問或完全不問的極端。我們在 System Prompt 中給予 AI 具體的提問邊界,避免它機械式地每漏一個條件就追問:
資訊足以處理時,needs_clarification 設為 false。
缺少會明顯影響結果的資訊時,needs_clarification 設為 true,並提出一個簡短問題。
商品推薦不要因為缺少每項偏好就機械式追問;只有缺少的資訊會明顯改變推薦結果時才問。
例如當顧客只說「我要退貨」卻沒有提供訂單編號時,AI 就會判定關鍵資訊缺漏,將 needs_clarification 設為 true,並主動產生「請提供要處理的訂單編號」的追問內容,讓系統可以停下來等待顧客補充。
要支援對話暫停與恢復,State 必須能記錄累積的對話、暫存使用者的最新回覆,以及追問的次數:
class State(BaseModel):
model_config = ConfigDict(extra="forbid")
message: str = Field(min_length=1, max_length=500) # 累積的對話全文
locale: Locale = Locale.ZH_TW
decision: IntentDecision | None = None # 當前輪次的 AI 決策
classification_error: str | None = None
pending_user_message: str | None = None # 暫存使用者剛補答的內容
clarification_count: int = Field(default=0, ge=0) # 已追問次數
handled_by: str | None = None
response: str = ""
status: Status = Status.NEW
在 builder.compile() 時必須掛載 checkpointer 保存狀態,且呼叫時傳入穩定的 thread_id:
graph = builder.compile(checkpointer=build_memory_checkpointer())
config = {
"configurable": {
"thread_id": "support-session-001", # 標識同一個對話 Session
}
}
意圖分類完成後,流程進入 workflow.py 的 Router。它的職責是檢查當前資訊是否足夠執行業務,決定要直接執行、停下來向使用者追問,還是轉接真人。
雖然我們在第二步已經透過 System Prompt 引導 AI 判斷 needs_clarification,但光靠提示詞是不夠的。AI 本質上是機率模型,面對情緒化、語意長或結構混亂的輸入時,仍有漏判必填欄位的風險。如果退款缺少單號卻被 AI 判定放行,後端 API 就會拋出例外。因此,執行業務所需的必要條件,必須在程式層由確定性的邏輯把關。
CONFIDENCE_THRESHOLD = 0.7
MAX_CLARIFICATIONS = 2
def route_after_classification(state: State) -> Route:
if state.classification_error is not None or state.decision is None:
return "classification_fallback"
decision = state.decision
if should_ask(decision):
# 防呆機制:若已追問達到上限,不再繼續追問,轉由真人接手
if state.clarification_count >= MAX_CLARIFICATIONS:
return "human_handoff"
return "ask_user"
return route_valid_decision(decision)
def should_ask(decision: IntentDecision) -> bool:
return (
requires_order_id(decision) # 1. 程式剛性把關:退款或查單缺少單號
or decision.confidence < CONFIDENCE_THRESHOLD # 2. 品質防線:意圖辨識信心不足(低於 0.7)
or decision.needs_clarification # 3. 採納 AI 建議:前面模型判定需要追問
)
def requires_order_id(decision: IntentDecision) -> bool:
return (
decision.intent in {Intent.ORDER_STATUS, Intent.REFUND}
and decision.order_id is None
)
should_ask 函式將兩種不同性質的判斷整合在同一個入口:
requires_order_id):退款或查單若缺少單號,不論 AI 主觀認為如何,程式一律強制判定需要追問。confidence < 0.7):AI 的把握度過低時,代表語意模糊或辨識困難,要求使用者重新描述。needs_clarification):如商品諮詢缺乏具體條件時,尊重模型在語意層的追問提議。當 should_ask 為 true 時,流程會走向 ask_user 發問;但若追問次數已達上限(本例為 2 次),Router 會果斷轉向 human_handoff 轉真人客服,避免系統無限詢問。如果都不符合,代表資料已備齊,流程便順利放行至對應的業務節點。
當 Router 決定走向 ask_user 節點時,我們挑選合適的問題,並呼叫 LangGraph 的 interrupt():
def clarification_question(state: State) -> str:
decision = require_decision(state)
# 程式政策產生的問題優先於 AI 問題
if requires_order_id(decision):
return POLICY_QUESTIONS[state.locale]["order_id"]
if decision.confidence < CONFIDENCE_THRESHOLD:
return POLICY_QUESTIONS[state.locale]["rephrase"]
if decision.needs_clarification and decision.clarification_question is not None:
return decision.clarification_question
raise ValueError("ask_user 找不到可用的補問內容")
def ask_user(state: State) -> dict[str, object]:
# interrupt 會在此處中斷執行,並將 question 回傳給呼叫端
answer = interrupt({"question": clarification_question(state)})
# 當外部以 Command(resume=...) 恢復時,answer 會取得傳入的值
return {"pending_user_message": str(answer)}
第一次觸發(觸發中斷):
result = graph.invoke(
{"message": "我要退款", "locale": "zh-TW"},
config=config,
version="v2",
)
# 從中斷事件中取得問題並顯示給使用者
print(result.interrupts[0].value["question"])
# 輸出:請提供 ORD-1234 格式的訂單編號。
此時 Graph 執行在 interrupt() 處暫停,State 被自動存入 Checkpointer,return 尚未執行。
使用者回答後(恢復執行):
result = graph.invoke(
Command(resume="ORD-1234"),
config=config,
version="v2",
)
LangGraph 會找到先前的 checkpoint,喚醒同一個節點,將 "ORD-1234" 作為 interrupt() 的返回值,接著執行 return {"pending_user_message": "ORD-1234"}。
當外部透過 Command(resume=...) 送回顧客的回答後,LangGraph 會依照設定的 Edge 將流程推進到 apply_answer 節點:
builder.add_edge("ask_user", "apply_answer") # 收到回答後,進入合併節點
builder.add_edge("apply_answer", "classify_intent") # 合併完成後,回到開頭重新分類
收到顧客的補充回答後,系統不能直接跳到退款或業務節點,因為補充的內容可能依然不完整。因此,流程必須回到 classify_intent,把補充後的完整對話重新交給分類器與 Router 評估一次。
在 apply_answer 節點內部,會呼叫 merge_answer 輔助函式將使用者的回答與原始需求串接,並在 State 中清除舊決策、遞增追問計數:
def merge_answer(original_message: str, answer: str) -> str:
merged = f"{original_message.strip()};使用者補充:{answer.strip()}"
return merged[:500] # 截斷長度,符合 State.message 的 max_length=500 限制
def apply_answer(state: State) -> dict[str, object]:
if state.pending_user_message is None:
raise ValueError("apply_answer 需要 pending_user_message")
return {
"message": merge_answer(state.message, state.pending_user_message),
"decision": None, # 清除舊決策
"classification_error": None,
"pending_user_message": None, # 清空暫存
"clarification_count": state.clarification_count + 1,
"status": Status.NEW,
}
例如原本顧客說「我要退掉昨天買的背包」,補答「ORD-1234」,合併後下一次 classify_intent 看到的輸入就是:
我要退掉昨天買的背包;使用者補充:ORD-1234
這樣一來,分類器在第二輪就能同時提取到退款意圖與訂單編號,讓 Router 能順利放行至後續的退款流程。
回顧從上一篇到本篇的多輪追問,無論是意圖的結構化驗證、關鍵欄位的格式檢查,還是何時該追問、何時該轉接真人,我們都選擇用確定性的程式碼(Pydantic 模型與 Router 條件分流)來表達,而不是全交給 AI 自由決定。
我們打造的是具有特定目的的 Agent,而不是毫無邊界的通用聊天機器人。這些特定目的——例如「退款必須持有單號」、「意圖不明時不能盲目放行」、「最多只能追問兩次」——正是系統的業務核心。
系統的核心價值與安全邊界,必須由工程師透過程式碼明確寫出來、強制執行。AI 的職責在於提供自然語言理解與彈性生成的輔助,狀態轉移與控制權限則牢牢掌握在程式手中。兩者各司其職,Agent 才能在保有對話彈性的同時,具備生產環境所必需的穩定度與可預測性。